/*
  S.M.A.C.K - An operating system kernel
  Copyright (C) 2010,2011 Mattias Holm and Kristian Rietveld
  For licensing and a full list of authors of the kernel, see the files
  COPYING and AUTHORS.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <process.h>
#include <syscall.h>
#include <errno.h>
#include <stdbool.h>
#include <elf.h>
#include <timer.h>

static bool
string_is_terminated(const char *buffer, size_t size)
{
  int i = 0;

  /* Ensure string is terminated. */
  for (i = 0; i < size; i++)
    if (buffer[i] == 0)
      break;

  if (i == size)
    return false;

  return true;
}


static int
syscall_sleep(void *args)
{
  struct sys_sleep *sleep_args = args;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_sleep)))
    return -EINVAL;

  /* When seconds is -1, we will simply relinquish the CPU until the
   * process gets scheduled again.  (We use this for EDF).
   */
  if (sleep_args->seconds == -1)
    proc_yield(proc_current());
  else
    proc_sleep(proc_current(), sleep_args->seconds);

  return 0;
}

static int
syscall_opendir(void *args)
{
  int i;
  struct sys_opendir *opendir_args = args;
  vfs_vnode_t *n;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_opendir)))
    return -EINVAL;

  if (!string_is_terminated(opendir_args->name, 256))
    return -EINVAL;

  n = vfs_getnode(opendir_args->name);
  if (!n)
    return -ENOENT;

  if (n->type == VFS_MOUNT)
    n = n->special.mount.root;

  if (n->type != VFS_DIR)
    return -ENOTDIR;

  i = proc_opendir(proc_current(), n);
  if (i < 0)
    return -EMFILE;

  return i;
}

static int
syscall_readdir(void *args)
{
  struct sys_readdir *readdir_args = args;
  vfs_file_t *f;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_readdir)))
    return -EINVAL;

  f = proc_current()->directory[readdir_args->handle];

  if (!f)
    return -EBADF;

  if (f->vnode->special.dir.files == NULL)
    vfs_readdir(f);

  if (f->off >= f->vnode->special.dir.files_in_dir)
    return -EEOF;

  readdir_args->type = f->vnode->special.dir.files[f->off]->type;
  readdir_args->size = f->vnode->special.dir.files[f->off]->size;
  strlcpy(readdir_args->name, f->vnode->special.dir.files[f->off]->name, 256);

  f->off++;

  return 0;
}

static int
syscall_closedir(void *args)
{
  struct sys_closedir *closedir_args = args;
  vfs_file_t *f;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_readdir)))
    return -EINVAL;

  f = proc_current()->directory[closedir_args->handle];

  if (!f)
    return -EBADF;

  proc_closedir(proc_current(), closedir_args->handle);

  return 0;
}

static int
syscall_exit(void *args)
{
  struct sys_exit *exit_args = args;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_exit)))
    return -EINVAL;

  proc_exit(proc_current(), exit_args->exit_code);

  /* proc_exit will never return. */

  return 0;
}

static int
syscall_open(void *args)
{
  int i;
  struct sys_open *open_args = args;
  vfs_vnode_t *n;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_open)))
    return -EINVAL;

  if (!string_is_terminated(open_args->name, 256))
    return -EINVAL;

  n = vfs_getnode(open_args->name);
  if (!n)
    return -ENOENT;

  i = proc_open(proc_current(), n);
  if (i < 0)
    return -EMFILE;

  return i;
}

static int
syscall_read(void *args)
{
  struct sys_read *read_args = args;
  vfs_file_t *f;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_read)))
    return -EINVAL;

  f = proc_current()->fd[read_args->handle];

  if (!f)
    return -EBADF;

  return vfs_read(f, read_args->buff, read_args->nbytes);
}

static int
syscall_write(void *args)
{
  struct sys_read *write_args = args;
  vfs_file_t *f;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_write)))
    return -EINVAL;

  f = proc_current()->fd[write_args->handle];

  if (!f)
    return -EBADF;

  return vfs_write(f, write_args->buff, write_args->nbytes);
}

static int
syscall_close(void *args)
{
  struct sys_close *close_args = args;
  vfs_file_t *f = proc_current()->fd[close_args->handle];

  if (!f)
    return -EBADF;

  proc_close(proc_current(), close_args->handle);

  return 0;
}

static int
syscall_seek(void *args)
{
  struct sys_seek *seek_args = args;
  vfs_file_t *f;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_seek)))
    return -EINVAL;

  f = proc_current()->fd[seek_args->handle];

  if (!f)
    return -EBADF;

  return vfs_seek(f, seek_args->offset, seek_args->whence);
}

static int
syscall_fork(void *args)
{
  proc_t *child;

  child = proc_dup(proc_current());
  if (!child)
    return -1;

  return child->pid;
}

static int
syscall_execve(void *args)
{
  char filename_copy[256];
  char argv_copy[256];
  struct sys_execve *execve_args = args;
  proc_t *proc = proc_current();

  if (!vm_valid_user_address_range(args, sizeof(struct sys_execve)))
    return -EINVAL;

  /* Check if filename is a valid executable */
  if (!elf_probe(execve_args->filename))
    return -EINVAL;

  /* We have to make copies, because the call to proc_unmap() will
   * destroy the stack which contains this data.
   */
  strlcpy(filename_copy, execve_args->filename, 256);
  strlcpy(argv_copy, execve_args->argv, 256);

  proc_unmap(proc);
  hw_proc_create_user_stack(proc);
  hw_proc_set_argv(proc, argv_copy, 256);
  return proc_load(proc, filename_copy);
}

static int
syscall_stat(void *args)
{
  struct sys_stat *stat_args = args;
  vfs_vnode_t *vnode;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_stat)))
    return -EINVAL;

  if (!string_is_terminated(stat_args->name, 256))
    return -EINVAL;

  vnode = vfs_getnode(stat_args->name);
  if (!vnode)
    return -ENOENT;

  stat_args->type = vnode->type;
  stat_args->size = vnode->size;

  return 0;
}

static int
syscall_wait(void *args)
{
  struct sys_wait *wait_args = args;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_wait)))
    return -EINVAL;

  return proc_wait(proc_current(), &wait_args->exit_code);
}

static int
syscall_chdir(void *args)
{
  struct sys_chdir *chdir_args = args;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_chdir)))
    return -EINVAL;

  if (!string_is_terminated(chdir_args->directory, 256))
    return -EINVAL;

  return proc_chdir(proc_current(), chdir_args->directory);
}

static int
syscall_getwd(void *args)
{
  struct sys_getwd *getwd_args = args;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_getwd)))
    return -EINVAL;

  proc_getwd(proc_current(), getwd_args->directory, 256);

  return 0;
}

static int
syscall_mount(void *args)
{
  vfs_vnode_t *node;
  struct sys_mount *mount_args = args;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_mount)))
    return -EINVAL;

  if (!string_is_terminated(mount_args->fs, 32))
    return -EINVAL;

  if (!string_is_terminated(mount_args->mountpoint, 256))
    return -EINVAL;

  node = vfs_getnode(mount_args->mountpoint);
  if (!node)
    return -ENOENT;

  vfs_mount(node, vfs_getfilesys(mount_args->fs), NULL, 0);

  return 0;
}

static int
syscall_get_processes(void *args)
{
  proc_fill_processes(args);

  return 0;
}

static int
syscall_ioctl(void *args)
{
  struct sys_ioctl *ioctl_args = args;
  vfs_file_t *f;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_ioctl)))
    return -EINVAL;

  f = proc_current()->fd[ioctl_args->handle];

  if (!f)
    return -EBADF;

  return vfs_ioctl(f, ioctl_args->request, ioctl_args->param);
}

static int
syscall_mmap(void *args)
{
  // Copy args in order to avoid luser overwriting the arguments while we are
  // in the kernel
  struct sys_mmap mmap_args;
  proc_t *p = proc_current();
  vm_map_t *vm = &p->vm_map;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_mmap)))
    return -EINVAL;

  mmap_args = *(struct sys_mmap*)args;

  // Prevent obvious abuse, i.e. addresse outside the vm map range
  if (mmap_args.addr) {
    if (!(vm->start <= (va_t)mmap_args.addr &&
          (va_t)mmap_args.addr < vm->start + vm->size)) {
      mmap_args.retval = NULL;
      return 0;
    }
    if (!((va_t)mmap_args.addr + mmap_args.len <= vm->start + vm->size)) {
      mmap_args.retval = NULL;
      return 0;
    }
  }

  if (mmap_args.flags == 0) {
    mmap_args.retval = vm_map(vm, VM_RW, (va_t)mmap_args.addr,
                              mmap_args.len);
  } else {
    mmap_args.retval = vm_map(vm, VM_RW|VM_CONTIG, (va_t)mmap_args.addr,
                              mmap_args.len);
  }
  // Copy back to user
  *(struct sys_mmap*)args = mmap_args;

  return 0;
}

static int
syscall_munmap(void *args)
{
  struct sys_munmap munmap_args;

  proc_t *p = proc_current();
  vm_map_t *vm = &p->vm_map;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_munmap)))
    return -EINVAL;

  munmap_args = *(struct sys_munmap*)args;

  if (munmap_args.addr) {
    if (!(vm->start <= (va_t)munmap_args.addr &&
          (va_t)munmap_args.addr < vm->start + vm->size)) {
      return -1;
    }
    if (!((va_t)munmap_args.addr + munmap_args.len <= vm->start + vm->size)) {
      return -1;
    }
  }

  vm_unmap(vm, munmap_args.addr);
  return 0;
}

static int
syscall_clock_gettime(void *args)
{
  struct sys_clock_gettime gettime_args;
  proc_t *p = proc_current();
  vm_map_t *vm = &p->vm_map;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_clock_gettime)))
    return -EINVAL;

  gettime_args = *(struct sys_clock_gettime*)args;

  if (!(vm->start <= (va_t)gettime_args.tp &&
        (va_t)gettime_args.tp < vm->start + vm->size - sizeof(struct timespec))) {
    return -1; // Invalid, may not specify kernel addresses
  }

  switch (gettime_args.clk_id) {
  case CLOCK_REALTIME: {
    return -1; // Not implemented
    break; // So we dont forget when it is implemented :)
  }
  case CLOCK_MONOTONIC: {
    // Only ms accuracy for now
    uint64_t internal_time = timer_get_system_ticks();
    gettime_args.tp->tv_sec = internal_time * CLOCK_RATE_MS / 1000;
    gettime_args.tp->tv_nsec = internal_time * CLOCK_RATE_MS % 1000 * 1000000;
    break;
  }
  case CLOCK_PROCESS_CPUTIME_ID: {
    uint64_t internal_time = p->cpu_time; // TODO: use accessor
    gettime_args.tp->tv_sec = internal_time * CLOCK_RATE_MS / 1000;
    gettime_args.tp->tv_nsec = internal_time * CLOCK_RATE_MS % 1000 * 1000000;
    break;
  }
  default:
    return -1; // Invalid clock type
  }

  // Copy back to user
  *(struct sys_clock_gettime*)args = gettime_args;
  return 0;
}

static int
syscall_clock_settime(void *args)
{
  struct sys_clock_settime settime_args;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_clock_gettime)))
    return -EINVAL;

  settime_args = *(struct sys_clock_settime*)args;

  return -1; // Not yet implemented
}

static int
syscall_setprocparam(void *args)
{
  struct sys_setprocparam settime_args;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_setprocparam)))
    return -EINVAL;

  settime_args = *(struct sys_setprocparam*)args;

  return -1;
}

static int
syscall_getprocparam(void *args)
{
  struct sys_getprocparam getprocparam_args;

  if (!vm_valid_user_address_range(args, sizeof(struct sys_getprocparam)))
    return -EINVAL;

  getprocparam_args = *(struct sys_getprocparam*)args;

  return -1;
}

static int
syscall_mkdir(void *args)
{
  int i;
  struct sys_mkdir *mkdir_args = (struct sys_mkdir *)args;
  vfs_vnode_t *n, *dir;
  char buffer[256];

  if (!string_is_terminated(mkdir_args->name, 256))
    return -EINVAL;

  n = vfs_getnode(mkdir_args->name);
  if (n)
    return -EEXIST;

  i = strnlen(mkdir_args->name, 256);
  for (; i >= 0; i--)
    if (mkdir_args->name[i] == '/')
      break;

  if (i == -1)
    /* Not an absolute path */
    return -EINVAL;

  strlcpy(buffer, mkdir_args->name, 256);
  buffer[i + 1] = 0;

  n = vfs_getnode(buffer);
  if (!n)
    return -ENOTDIR;

  strlcpy(buffer, mkdir_args->name + i + 1, 256);
  dir = vfs_mkdir(n, buffer);
  return dir == NULL ? -EIO : 0;
}

static int
syscall_rmdir(void *args)
{
  struct sys_rmdir *rmdir_args = (struct sys_rmdir *)args;
  vfs_vnode_t *n;

  if (!string_is_terminated(rmdir_args->name, 256))
    return -EINVAL;

  n = vfs_getnode(rmdir_args->name);
  if (!n)
    return -ENOENT;

  if (n->type != VFS_DIR)
    return -ENOTDIR;

  if (!n->special.dir.files)
    n->fops->readdir(n, &n->special.dir.files_in_dir, &n->special.dir.files);

  if (n->special.dir.files_in_dir > 0)
    return -ENOTEMPTY;

  return vfs_rmdir(n);
}


typedef int (* syscall_handler) (void *);

static syscall_handler syscall_jump_table[64] =
{
  NULL,
  syscall_sleep,
  syscall_opendir,
  syscall_readdir,
  syscall_closedir,
  syscall_exit,
  syscall_open,
  syscall_read,
  syscall_write,
  syscall_close,
  syscall_seek, /* 10 */
  syscall_fork,
  syscall_execve,
  syscall_stat,
  syscall_wait,
  syscall_chdir,
  syscall_getwd,
  syscall_mount,
  syscall_get_processes,
  syscall_ioctl,  /* FIXME: should be next to open/read/write actually */
  syscall_mmap, /* 20 */
  syscall_munmap,
  syscall_clock_gettime,
  syscall_clock_settime,
  syscall_setprocparam,
  syscall_getprocparam,
  syscall_mkdir,
  syscall_rmdir,
  NULL
};

int
syscall(unsigned callId, void *args)
{
  if (syscall_jump_table[callId] == NULL)
    {
      printf("ERROR: Process %p called unimplemented system call %d\n",
             proc_current(), callId);
      return -EINVAL;
    }

  return (* syscall_jump_table[callId]) (args);
}
